package bloom

import (
	"bytes"
	"encoding/binary"
	"github.com/liyue201/gostl/algorithm/hash"
	"github.com/liyue201/gostl/ds/bitmap"
	"github.com/liyue201/gostl/utils/sync"
	"math"
	gosync "sync"
)

const salt = "g9hmj2fhgr"

var defaultLocker sync.FakeLocker

// Options holds BloomFilter's options
type Options struct {
	locker sync.Locker
}

// Option is a function type used to set Options
type Option func(opt *Options)

// WithGoroutineSafe is used to config a BloomFilter with goroutine-safe
func WithGoroutineSafe() Option {
	return func(opt *Options) {
		opt.locker = &gosync.RWMutex{}
	}
}

// BloomFilter is an implementation of bloom filter
type BloomFilter struct {
	m      uint64
	k      uint64
	b      *bitmap.Bitmap
	locker sync.Locker
}

// New creates a new BloomFilter with m bits and k hash functions
func New(m, k uint64, opts ...Option) *BloomFilter {
	opt := Options{
		locker: defaultLocker,
	}
	for _, o := range opts {
		o(&opt)
	}
	return &BloomFilter{
		m:      m,
		k:      k,
		b:      bitmap.New(m),
		locker: opt.locker,
	}
}

// NewWithEstimates creates a new BloomFilter with n and fp.
// n is the capacity of the BloomFilter
// fp is the tolerated error rate of the BloomFilter
func NewWithEstimates(n uint64, fp float64, opts ...Option) *BloomFilter {
	m, k := EstimateParameters(n, fp)
	return New(m, k, opts...)
}

// NewFromData creates a new BloomFilter from data generated by function 'Data()'
func NewFromData(data []byte, opts ...Option) *BloomFilter {
	opt := Options{
		locker: defaultLocker,
	}
	for _, o := range opts {
		o(&opt)
	}
	b := &BloomFilter{
		locker: opt.locker,
	}
	reader := bytes.NewReader(data)
	binary.Read(reader, binary.LittleEndian, &b.m)
	binary.Read(reader, binary.LittleEndian, &b.k)
	b.b = bitmap.NewFromData(data[8+8:])
	return b
}

// EstimateParameters estimates m and k from n and p
func EstimateParameters(n uint64, p float64) (m uint64, k uint64) {
	m = uint64(math.Ceil(-1 * float64(n) * math.Log(p) / (math.Ln2 * math.Ln2)))
	k = uint64(math.Ceil(math.Ln2 * float64(m) / float64(n)))
	return
}

// Add adds val to the BloomFilter
func (bf *BloomFilter) Add(val string) {
	bf.locker.Lock()
	defer bf.locker.Unlock()

	hashs := hash.GenHashInts([]byte(salt+val), int(bf.k))
	for i := uint64(0); i < bf.k; i++ {
		bf.b.Set(hashs[i] % bf.m)
	}
}

// Contains returns true if val is (high probability) in the BloomFilter, otherwise returns false.
func (bf *BloomFilter) Contains(val string) bool {
	bf.locker.RLock()
	defer bf.locker.RUnlock()

	hashs := hash.GenHashInts([]byte(salt+val), int(bf.k))
	for i := uint64(0); i < bf.k; i++ {
		if !bf.b.IsSet(hashs[i] % bf.m) {
			return false
		}
	}
	return true
}

// Data returns the data of the BloomFilter, it can bee used to creates a new BloomFilter by using function 'NewFromData' .
func (bf *BloomFilter) Data() []byte {
	bf.locker.Lock()
	defer bf.locker.Unlock()

	buf := new(bytes.Buffer)
	binary.Write(buf, binary.LittleEndian, bf.m)
	binary.Write(buf, binary.LittleEndian, bf.k)
	buf.Write(bf.b.Data())
	return buf.Bytes()
}
